﻿using System;
using System.Runtime.InteropServices;

namespace pub.Interop
{
    public class SafeArrayMarshaler
    {
        // a SAFEARRAY bound layout
        [StructLayout(LayoutKind.Sequential, Pack = 4)]
        public struct SafeArrayBound
        {
            [ComAliasName("Microsoft.VisualStudio.OLE.Interop.ULONG")] public uint cElements;
            [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")] public int lLbound;
        }

        #region ICustomMarshaler members

        public object MarshalNativeToManaged(IntPtr pNativeData)
        {
            if (pNativeData == IntPtr.Zero)
                return null;

            // check that dimension number is one (this marshaller does not manage multi-dimensionnal SAFEARRAYs)
            if (SafeArrayGetDim(pNativeData) != 1)
                throw new ArgumentException(
                        "pNativeData must point to a SAFEARRAY with exactly one dimension",
                        "pNativeData");

            // get lower bound
            long lBound = 0;
            var hr = SafeArrayGetLBound(pNativeData, 1, ref lBound);
            if (hr != 0)
                throw Marshal.GetExceptionForHR(hr);

            // check that lower bound is 0 (this marshaller does not manage non 0 lower bound)
            if (lBound != 0)
                throw new ArgumentException(
                        "pNativeData must point to a SAFEARRAY with one dimension starting at 0",
                        "pNativeData");

            // get upper bound
            long uBound = 0;
            hr = SafeArrayGetUBound(pNativeData, 1, ref uBound);
            if (hr != 0)
                throw Marshal.GetExceptionForHR(hr);

            // get the memory size of a single element
            var elementSize = SafeArrayGetElemsize(pNativeData);
            if (elementSize == 0)
                throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());

            // access (and lock) data
            var data = IntPtr.Zero;
            hr = SafeArrayAccessData(pNativeData, ref data);
            if (hr != 0)
                throw Marshal.GetExceptionForHR(hr);

            try
            {
                // use the handy System.Runtime.InteropServices.Marshal class
                var returned = Marshal.GetObjectsForNativeVariants(data, (int) (uBound + 1));

                // From now the returned object is good
                // I add my personnal need : I want to translate NULL strings to empty strings, and NULL dates to default
                // value of OLE dates

                for (var i = 0; i <= uBound; ++i)
                {
                    // get the address of the element
                    var ptr = new IntPtr(data.ToInt32() + elementSize*i);

                    // read the first 16 bits, as the VAR_TYPE is on two bytes
                    var ve = (VarEnum) Marshal.ReadInt16(ptr);

                    // process
                    if (ve == VarEnum.VT_BSTR && returned[i] == null)
                    {
                        returned[i] = String.Empty;
                    }
                    else if (ve == VarEnum.VT_DATE && returned[i] == null)
                    {
                        returned[i] = DateTime.FromOADate(0);
                    }
                }

                return returned;
            }
            finally
            {
                // SafeArrayAccessData locks the array
                SafeArrayUnaccessData(pNativeData);
            }
        }

        // data returned by MarshalManagedToNative, so I will be able to clean it in CleanUpNativeData
        private IntPtr _safeArrayPtr;

        public IntPtr MarshalManagedToNative(object managedObj)
        {
            if (managedObj == null)
                return IntPtr.Zero;

            if (!(managedObj is object[]))
                throw new ArgumentException("Input must be of object[] type", "managedObj");

            var realObject = (object[]) managedObj;

            try
            {
                // Create the SAFEARRAY

                // create my unique dimension
                SafeArrayBound sab;
                sab.lLbound = 0;
                sab.cElements = (uint) realObject.Length;

                var psab = IntPtr.Zero;
                try
                {
                    // Performances would be better with a stackalloc, but then I would have to declare the method unsafe
                    psab = Marshal.AllocHGlobal(Marshal.SizeOf(sab));
                    Marshal.StructureToPtr(sab, psab, false);

                    // the real SAFEARRAY creation
                    _safeArrayPtr = SafeArrayCreate((ushort) VarEnum.VT_VARIANT, 1, psab);
                    if (_safeArrayPtr == IntPtr.Zero)
                        throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
                }
                finally
                {
                    // SafeArrayCreate create a copy of the SafeArrayBound memory, so it is safe and recommended to free it here
                    if (psab != IntPtr.Zero)
                        Marshal.FreeHGlobal(psab);
                }

                var data = IntPtr.Zero;

                // I use SafeArrayAccessData instead of SafeArrayPutElement to avoid one allocation for each Variant
                var hr = SafeArrayAccessData(_safeArrayPtr, ref data);
                if (hr != 0)
                    throw Marshal.GetExceptionForHR(hr);

                try
                {
                    // get the memory size of a single element
                    var elementSize = SafeArrayGetElemsize(_safeArrayPtr);
                    if (elementSize == 0)
                        throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());

                    // use the handy System.Runtime.InteropServices.Marshal class for each element
                    for (var i = 0; i < realObject.Length; ++i)
                    {
                        // get the address of the element
                        var ptr = new IntPtr(data.ToInt32() + elementSize*i);

                        Marshal.GetNativeVariantForObject(realObject[i], ptr);
                    }
                }
                finally
                {
                    // SafeArrayAccessData locks the array
                    SafeArrayUnaccessData(_safeArrayPtr);
                }

                return _safeArrayPtr;
            }
            catch (Exception)
            {
                CleanUpNativeData(_safeArrayPtr);
                _safeArrayPtr = IntPtr.Zero;
                throw;
            }
        }

        // Clean the data returned by MarshalManagedToNative
        public void CleanUpNativeData(IntPtr pNativeData)
        {
            if (pNativeData != IntPtr.Zero)
            {
                var hr = SafeArrayDestroy(pNativeData);
                if (hr != 0)
                    throw Marshal.GetExceptionForHR(hr);
            }
        }

        // Managed datas will be cleaned by the garbage collector
        public void CleanUpManagedData(object managedObj)
        {
        }

        // As my marshaller manage only a reference value
        public int GetNativeDataSize()
        {
            return -1;
        }

        #endregion

        #region pInvoke stuff. For your personnal project, it is better to put it on a separate class

        [DllImport("oleaut32.dll", EntryPoint = "SafeArrayCreate", SetLastError = true,
                CharSet = CharSet.Auto,
                ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern IntPtr SafeArrayCreate(ushort vt, uint cDims, IntPtr rgsabound);

        [DllImport("oleaut32.dll", EntryPoint = "SafeArrayDestroy", SetLastError = true,
                CharSet = CharSet.Auto,
                ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern int SafeArrayDestroy(IntPtr psa);

        [DllImport("oleaut32.dll", EntryPoint = "SafeArrayAccessData", SetLastError = true,
                CharSet = CharSet.Auto,
                ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern int SafeArrayAccessData(IntPtr psa, ref IntPtr ppvData);

        [DllImport("oleaut32.dll", EntryPoint = "SafeArrayUnaccessData", SetLastError = true,
                CharSet = CharSet.Auto,
                ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern int SafeArrayUnaccessData(IntPtr psa);

        [DllImport("oleaut32.dll", EntryPoint = "SafeArrayGetElemsize", SetLastError = true,
                CharSet = CharSet.Auto,
                ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern uint SafeArrayGetElemsize(IntPtr psa);

        [DllImport("oleaut32.dll", EntryPoint = "SafeArrayGetDim", SetLastError = true,
                CharSet = CharSet.Auto,
                ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern uint SafeArrayGetDim(IntPtr psa);

        [DllImport("oleaut32.dll", EntryPoint = "SafeArrayGetLBound", SetLastError = true,
                CharSet = CharSet.Auto,
                ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern int SafeArrayGetLBound(IntPtr psa, uint nDim, ref long bound);

        [DllImport("oleaut32.dll", EntryPoint = "SafeArrayGetUBound", SetLastError = true,
                CharSet = CharSet.Auto,
                ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern int SafeArrayGetUBound(IntPtr psa, uint nDim, ref long bound);

        #endregion
    }
}